Skip to content

Date 与常用工具函数

作者:青见春山
发表于:2026-07-29
字数统计:7000 字
预计阅读24分钟

涵盖 Date 对象核心方法、倒计时实现、变量提升陷阱、JS 中的常用工具函数合集。

一、Date 对象

1. 创建 Date

JavaScript
// 1. 无参数(当前时间)
const now = new Date()
console.log(now)  // 当前时间

// 2. 时间戳(毫秒)
const date1 = new Date(1715000000000)
const date2 = new Date(0)  // 1970-01-01T00:00:00.000Z

// 3. 日期字符串(注意 ISO 格式会按 UTC 解析)
const date3 = new Date('2024-05-15T10:30:00')  // UTC
const date4 = new Date('2024-05-15')          // UTC 0 点
const date5 = new Date('2024/05/15')           // 本地时间
const date6 = new Date('May 15, 2024')        // 本地时间

// 4. 数字参数(注意月份从 0 开始)
const date7 = new Date(2024, 4, 15, 10, 30, 0)  // 2024年5月15日 10:30:00(本地)
// 月份是 0 = 一月,4 = 五月

2. 获取日期信息

JavaScript
const d = new Date(2024, 4, 15, 10, 30, 45)

d.getFullYear()        // 2024(年)
d.getMonth()           // 4(五月,0-11)
d.getDate()            // 15(月中第几天,1-31)
d.getDay()             // 3(星期三,0-6,0 = 周日)
d.getHours()           // 10(小时,0-23)
d.getMinutes()         // 30(分钟,0-59)
d.getSeconds()         // 45(秒,0-59)
d.getMilliseconds()    // 0(毫秒,0-999)

// UTC 版本(注意大小写是 UTC,方法名是大写 UTC)
d.getUTCFullYear()     // 2024
d.getUTCMonth()        // 4(按 UTC 时区算)

// 时间戳
d.getTime()            // 1715753445000(毫秒时间戳)
Date.now()             // 当前时间戳

3. 设置日期信息

JavaScript
const d = new Date()

d.setFullYear(2025)
d.setMonth(0)          // 设置为一月
d.setDate(1)           // 设置为一号
d.setHours(0, 0, 0, 0) // 设为 0 点 0 分 0 秒

// 链式设置(因为 set 方法返回时间戳)
d.setFullYear(2025).setMonth(0).setDate(1)

4. 日期格式化

JavaScript
// 1. 内置方法
const d = new Date()
d.toString()        // 'Wed May 15 2024 10:30:45 GMT+0800 (中国标准时间)'
d.toDateString()    // 'Wed May 15 2024'
d.toTimeString()    // '10:30:45 GMT+0800 (中国标准时间)'
d.toISOString()     // '2024-05-15T02:30:45.000Z'(UTC)
d.toJSON()          // 等同于 toISOString
d.toLocaleString()  // '2024/5/15 10:30:45'(按本地区)
d.toLocaleDateString()  // '2024/5/15'
d.toLocaleTimeString()  // '10:30:45'

// 2. 自定义格式化(YYYY-MM-DD HH:mm:ss)
function formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
  if (!(date instanceof Date)) date = new Date(date)

  const map = {
    YYYY: date.getFullYear(),
    MM: String(date.getMonth() + 1).padStart(2, '0'),
    DD: String(date.getDate()).padStart(2, '0'),
    HH: String(date.getHours()).padStart(2, '0'),
    mm: String(date.getMinutes()).padStart(2, '0'),
    ss: String(date.getSeconds()).padStart(2, '0')
  }

  return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, key => map[key])
}

formatDate(new Date())  // '2024-05-15 10:30:45'
formatDate(new Date(), 'YYYY/MM/DD')  // '2024/05/15'

// 3. 高级格式化推荐方案:使用 dayjs、date-fns
// 不要重复造轮子,复杂的日期逻辑交给专业库

5. Date 的常见陷阱

JavaScript
// 1. 月份从 0 开始
new Date(2024, 5, 15)  // 6 月 15 日(不是 5 月)

// 2. 时间戳是毫秒,不是秒
Date.now()                  // 1715753445000
Math.floor(Date.now() / 1000)  // 秒级时间戳

// 3. Date 解析的时区问题
new Date('2024-05-15')           // UTC 0 点
new Date('2024-05-15T00:00:00')  // UTC 0 点
new Date(2024, 4, 15)            // 本地时间 0 点

// 4. getTime 在 -1900 之前的整数会被视为年份
new Date(99, 0)  // 1900 + 99 年 = 1999
new Date(2024, 0)  // 2024 年

二、倒计时实现

1. 基础倒计时

JavaScript
function countdown(targetTime) {
  const now = Date.now()
  const diff = targetTime - now  // 毫秒差

  if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true }

  const days = Math.floor(diff / (1000 * 60 * 60 * 24))
  const hours = Math.floor(diff / (1000 * 60 * 60) % 24)
  const minutes = Math.floor(diff / (1000 * 60) % 60)
  const seconds = Math.floor(diff / 1000 % 60)

  return { days, hours, minutes, seconds, expired: false }
}

// 用 setInterval 每秒刷新
const target = new Date('2025-01-01T00:00:00').getTime()
setInterval(() => {
  const { days, hours, minutes, seconds } = countdown(target)
  console.log(`距离 2025 年还有 ${days} 天 ${hours} 时 ${minutes} 分 ${seconds} 秒`)
}, 1000)

2. 跨时区倒计时

JavaScript
// 同一时刻,不同时区显示不同时间
function getLocalTime(utcTime, targetTimezone) {
  // UTC 时间的字符串
  const utcStr = new Date(utcTime).toLocaleString('en-US', { timeZone: 'UTC' })
  // 转成目标时区时间
  const localDate = new Date(utcStr)

  return new Intl.DateTimeFormat('zh-CN', {
    timeZone: targetTimezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false
  }).format(localDate)
}

getLocalTime(Date.now(), 'Asia/Shanghai')   // '2024/05/15 10:30:45'
getLocalTime(Date.now(), 'America/New_York')  // '2024/05/14 22:30:45'

3. 性能更优的倒计时

JavaScript
// 倒计时 1 秒的 0-2ms 是允许的,可以不用 setInterval,用 requestAnimationFrame 或 setTimeout 链式
function startCountdown(targetTime, onTick, onComplete) {
  let timer = null

  function tick() {
    const now = Date.now()
    const diff = targetTime - now

    if (diff <= 0) {
      onComplete?.()
      return
    }

    onTick?.(diff)
    timer = setTimeout(tick, 1000 - (diff % 1000))  // 对齐到整数秒
  }

  tick()

  return () => clearTimeout(timer)  // 返回取消函数
}

三、变量提升与函数提升

1. 经典坑题

JavaScript
// 题目 1
console.log(a)        // undefined(不是 ReferenceError)
var a = 1

编译阶段展开(实际执行顺序)

JavaScript
// 引擎看到 var,编译阶段把 a 提升到当前作用域顶部,赋值为 undefined
// console.log 看到的是声明提升后的默认值

// 编译后:
var a                  // a 已声明但未赋值
console.log(a)          // undefined
a = 1

2. 函数提升的优先级

JavaScript
// 函数声明 > 变量声明
console.log(typeof fn)

function fn() {}
var fn = 'hello'       // 运行时赋值
console.log(typeof fn)

实际编译后:

JavaScript
function fn() {}        // 函数声明先提升
var fn                  // 同名变量,提升但不会覆盖函数
console.log(typeof fn)  // 'function'
fn = 'hello'            // 运行时赋值,函数被覆盖
console.log(typeof fn)  // 'string'

3. let / const 的"暂时性死区"(TDZ)

JavaScript
console.log(a)  // ❌ ReferenceError
let a = 1

console.log(b)  // ❌ ReferenceError(const 同理)
const b = 2

// 关键:let/const 也会提升,但**不能访问**——从作用域开始到声明之间的区域称为 TDZ(Temporal Dead Zone)

4. 函数声明 vs 函数表达式的提升

JavaScript
foo()  // ✅ 'function'  // 函数声明整体提升

function foo() {
  console.log('function')
}
JavaScript
foo()  // ❌ TypeError: foo is not a function(变量已声明但未赋值)

var foo = function() {
  console.log('expression')
}
// 编译提升:var foo(undefined)
// 执行到 foo() 时 foo 还是 undefined

// 或 let 的话直接 ReferenceError

5. 块级作用域

JavaScript
// 在 ES5 中,var 没有块级作用域
for (var i = 0; i < 3; i++) {
  // ...
}
console.log(i)  // 3(泄漏到外部)

// ES6 的 let / const 有块级作用域
for (let i = 0; i < 3; i++) {
  // ...
}
console.log(i)  // ReferenceError

// 这是经典的"for 循环闭包问题"的解决方案

6. 综合陷阱题

JavaScript
// 题目
var a = 1
function fn() {
  console.log(a)  // undefined(不是全局的 1!)
  var a = 2
}
fn()

// 编译后:
// var a        // 全局
// a = 1
// function fn() { var a; console.log(a); a = 2 }
// fn 执行时,作用域内有 a = undefined,赋值发生在 console 之后
JavaScript
// 题目:循环 + 定时器
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 输出 3, 3, 3

// 解决方案 1:let 形成块级作用域
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 输出 0, 1, 2

// 解决方案 2:闭包保存
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => console.log(j), 0)
  })(i)
}
// 输出 0, 1, 2

四、JS 常用工具函数

1. 类型判断

JavaScript
// 通用 typeof
typeof 123        // 'number'
typeof 'string'   // 'string'
typeof null        // 'object'(经典坑)
typeof undefined   // 'undefined'
typeof []          // 'object'
typeof {}          // 'object'

// 精确类型判断
function getType(val) {
  if (val === null) return 'null'
  return Object.prototype.toString.call(val).slice(8, -1).toLowerCase()
}

getType([])        // 'array'
getType({})        // 'object'
getType(new Date())  // 'date'
getType(/foo/)     // 'regexp'
getType(null)      // 'null'
getType(undefined) // 'undefined'
getType(new Map()) // 'map'

// 简洁版
const isArr = Array.isArray
const isObj = v => typeof v === 'object' && v !== null

2. 深拷贝(手写版)

JavaScript
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  if (hash.has(obj)) return hash.get(obj)

  const cloneObj = Array.isArray(obj) ? [] : {}
  hash.set(obj, cloneObj)

  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  return cloneObj
}

3. 防抖

JavaScript
function debounce(fn, delay) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// 立即执行版(第一次触发立刻执行,之后等 delay)
function debounceImmediate(fn, delay) {
  let timer = null
  return function(...args) {
    const callNow = !timer
    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
    }, delay)
    if (callNow) fn.apply(this, args)
  }
}

4. 节流

JavaScript
// 时间戳版(首次立刻执行,结束后不执行)
function throttle(fn, delay) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// 定时器版(首次延迟执行,结束后再执行一次)
function throttleTimer(fn, delay) {
  let timer = null
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null
        fn.apply(this, args)
      }, delay)
    }
  }
}

5. 数组去重

JavaScript
// 方法1:Set
const unique = arr => [...new Set(arr)]

// 方法2:filter
const unique2 = arr => arr.filter((v, i) => arr.indexOf(v) === i)

// 方法3:reduce
const unique3 = arr => arr.reduce((acc, v) => acc.includes(v) ? acc : [...acc, v], [])

// 对象数组去重(按某个字段)
const uniqueBy = (arr, key) => {
  const seen = new Set()
  return arr.filter(item => {
    const k = item[key]
    return seen.has(k) ? false : seen.add(k)
  })
}

6. 数组扁平化

JavaScript
// 方法1:flat(ES2019)
arr.flat(Infinity)

// 方法2:reduce + 递归
function flatten(arr) {
  return arr.reduce((acc, v) => acc.concat(Array.isArray(v) ? flatten(v) : v), [])
}

// 方法3:toString(仅适用于基本类型)
arr.toString().split(',')  // 注意会改变类型,且对 null/undefined 异常

7. URL 参数解析

JavaScript
// URLSearchParams(推荐)
function getQueryParams(url = window.location.search) {
  const params = new URLSearchParams(url)
  const result = {}
  for (const [key, value] of params) {
    result[key] = value
  }
  return result
}

// 手写解析
function parseQuery(url = window.location.search) {
  const query = url.slice(1)  // 去掉 '?'
  const obj = {}
  query.split('&').forEach(item => {
    if (!item) return
    const [key, value = ''] = item.split('=')
    obj[decodeURIComponent(key)] = decodeURIComponent(value)
  })
  return obj
}

8. 千分位

JavaScript
function thousands(num) {
  const parts = String(num).split('.')
  parts[0] = parts[0].replace(/\B(?=(\d{3})+$)/g, ',')
  return parts.join('.')
}

thousands(1234567)         // '1,234,567'
thousands(1234567.89)      // '1,234,567.89'
thousands(1234567.891234)  // '1,234,567.891234'

// 或用 Intl
new Intl.NumberFormat().format(1234567)  // '1,234,567'(取决于 locale)

9. 随机 ID

JavaScript
function randomId(length = 8) {
  return Math.random().toString(36).slice(2, 2 + length)
}

randomId()    // 'k3j1h2g4'
randomId(16)  // 'k3j1h2g4a9b0c1d2'

// 更高质量:crypto.randomUUID(现代浏览器和 Node 14+)
crypto.randomUUID()  // '550e8400-e29b-41d4-a716-446655440000'

10. 对象深比较

JavaScript
function isEqual(a, b) {
  if (a === b) return true
  if (typeof a !== typeof b) return false
  if (typeof a !== 'object' || a === null || b === null) return false

  const aKeys = Object.keys(a)
  const bKeys = Object.keys(b)
  if (aKeys.length !== bKeys.length) return false

  return aKeys.every(key => isEqual(a[key], b[key]))
}

五、面试高频问答

Q1: 如何实现一个倒计时?

  1. 计算目标时间与当前时间的差值(毫秒)
  2. 把毫秒换算成天 / 时 / 分 / 秒
  3. setInterval 或对齐到秒的 setTimeout 链式调用,每秒更新
  4. 注意页面隐藏时 setInterval 会节流优化(最小 1s),但不影响倒计时精度
  5. 组件销毁记得清理定时器,防止内存泄漏

Q2: 解释一下 JS 的变量提升?

:JS 引擎在编译阶段把 var 声明和 function 声明提升到当前作用域顶部,但赋值留在原地。let / const 也会"提升",但从作用域开始到声明之间的区域称为暂时性死区(TDZ),访问会抛 ReferenceError

Q3: 为什么变量提升对闭包问题有影响?

for 循环用 var 声明变量时,所有循环共享同一个变量引用,定时器回调执行时,循环早已结束,i 已经是最终值。用 let 声明变量会形成独立的块级作用域,每次循环都创建一个新的绑定。详见 闭包与作用域

六、关联文档